package edu.rice.myxo.utils;

import java.io.*;
import java.util.Random;
import java.util.Scanner;

public class GA {

	/**
	 * Take in the parent parameters param and parameter boundary bound, mutate
	 * single parameter with mutation rate and cross parameter sets with
	 * rateCross. N is the number of offspring
	 * 
	 * Visit each parameter, first use probability of rateMut to decide if this
	 * parameter is going to mutate. Second, if is parameter is chosen to be
	 * mutated, then decide with probability of 0.5 whether it is a positive
	 * mutate or a negative mutate. Third, from the parameter value to the
	 * parameter bound or parameter value plus (or minus) the step size, which
	 * is near, use uniform distribution to find the mutated parameter.
	 * 
	 * Visit each pair of parameter sets (parents), first use rateCross to
	 * decide if the two parameter sets to be crossed. Second the new parameter
	 * sets is 3p1/2-1p2/2, 3p2/2-1p1/2.
	 * 
	 * 
	 * @param param
	 *            Should be N*M N parent and M parameter per parent
	 * @param bound
	 *            Should be 3*M, each parameter has a upper bound, lower bound
	 *            and a step size
	 * @param rateMut
	 *            Mutaion rate
	 * @param rateCross
	 *            Cross rate
	 * @param N
	 *            Number of offspring
	 * @return
	 */
	public double[][] mutParams(double[][] param, double[][] bound,
			double rateMut, double rateCross, int N) {
		Random r = new Random();
		r.setSeed(System.currentTimeMillis());
		// Mutation
		for (int i = 0; i < param.length; i++) {
			param[i] = mutSingleParam(param[i], bound, rateMut);
		}
		// Cross
		for (int i = 0; i < param.length - 1; i++) {
			if (r.nextDouble() < rateCross) {
				for (int j = i + 1; j < param.length; j++) {

					for (int k = 0; k < param[0].length; k++) {
						double value1 = param[i][k] / 2 + param[j][k] / 2;
						double value2 = 3 * param[i][k] / 2 - param[j][k] / 2;
						double value3 = 3 * param[j][k] / 2 - param[i][k] / 2;
						param[i][k] = value1;
						if (value2 > bound[k][1] && value2 < bound[k][0]) {
							param[j][k] = value2;
						} else if (value3 > bound[k][1] && value3 < bound[k][0]) {
							param[j][k] = value3;
						} else {
							param[j][k] = value1;
						}
					}
				}
			}
		}
		return param;
	}

	public double[] mutSingleParam(double[] p, double[][] bound, double rateMut) {
		double[] np = new double[p.length];
		Random r = new Random();
		r.setSeed(System.currentTimeMillis());
		for (int i = 0; i < p.length; i++) {
			if (r.nextDouble() < rateMut) {
				if (r.nextDouble() < 0.5) {
					double lowBound = bound[i][1] < (p[i] - bound[i][2]) ? (p[i] - bound[i][2])
							: bound[i][1];
					np[i] = r.nextDouble() * (p[i] - lowBound) + lowBound;
				} else {
					double upBound = bound[i][0] > (p[i] + bound[i][2]) ? (p[i] + bound[i][2])
							: bound[i][0];
					np[i] = r.nextDouble() * (upBound - p[i]) + p[i];
				}
			} else {
				np[i] = p[i];
			}
		}
		return np;
	}

	public double calFit(double[] p) {
		double f = 0;
		for (int i = 0; i < p.length; i++) {
			f += p[i] * p[i];
		}
		return f;
	}

	public double[][] sortArray(double[] a) {
		double[][] indValue = new double[2][a.length];
		double[] ind0 = new double[a.length];
		for (int i = 0; i < a.length; i++) {
			ind0[i] = (double) i;
		}
		for (int i = 0; i < a.length - 1; i++) {
			for (int j = i + 1; j < a.length; j++) {
				if (a[i] < a[j]) {
					double temp = a[j];
					a[j] = a[i];
					a[i] = temp;
					temp = ind0[j];
					ind0[j] = ind0[i];
					ind0[i] = temp;
				}
			}
		}
		indValue[0] = a;
		indValue[1] = ind0;
		return indValue;
	}

	public void writeParam(double[][] p, String paramFn) {
		try {
			FileWriter fw = new FileWriter(paramFn, true);
			PrintWriter pw = new PrintWriter(fw);
			for (int i = 0; i < p.length; i++) {
				for (int j = 0; j < p[0].length; j++) {
					if (j == p[0].length - 1) {
						pw.format("%f", p[i][j]);
					} else {
						pw.format("%f,", p[i][j]);
					}

				}
				pw.println();
			}

			pw.close();
			fw.close();
		} catch (IOException e) {
		}
	}

	public void writeSingleParam(double[][] p, String paramFn) {
		try {
			for (int i = 0; i < p.length; i++) {
				String fn = paramFn + "/param" + i;
				FileWriter fw = new FileWriter(fn);
				PrintWriter pw = new PrintWriter(fw);
				for (int j = 0; j < p[0].length; j++) {
					pw.println(p[i][j]);
				}
				pw.close();
				fw.close();
			}
		} catch (IOException e) {
		}
	}

	public void writeSingleParam(double[][] p, int[] logInd, String[] varName,
			String folder) {
		try {
			for (int i = 0; i < p.length; i++) {
				String fn = folder + "/param" + i;
				FileWriter fw = new FileWriter(fn);
				PrintWriter pw = new PrintWriter(fw);
				for (int j = 0; j < p[0].length; j++) {
					if (logInd[j] == 0) {
						pw.println(varName[j] + "=" + p[i][j]);
					} else {
						pw.println(varName[j] + "=" + Math.exp(p[i][j]));
					}
				}
				fw.close();
				pw.close();
			}
		} catch (IOException e) {
		}
	}

	public void writeFitness(double[] f, String fitnessFn) {
		try {
			FileWriter fw = new FileWriter(fitnessFn);
			PrintWriter pw = new PrintWriter(fw);
			double meanf = 0;
			for (int i = 0; i < f.length; i++) {
				meanf += f[i];
				pw.println(f[i]);
			}
			pw.println("Average fitness is=" + (meanf / (double) f.length));
			pw.close();
			fw.close();
		} catch (IOException e) {
		}
	}

	public GA(int G, double rateMut) {
		// Number of params
		int N = 9;
		// Number of population
		int M0 = 60;
		// Number of bottleneck population
		int M = 10;
		double[][] bound = new double[N][3];
		for (int i = 0; i < N; i++) {
			bound[i][0] = 1;
			bound[i][1] = 0;
			bound[i][2] = 0.01;
		}
		for (int g = 0; g < G; g++) {
			if (g == 0) {
				// Prepare folder and files
				String rootFolder = "ga";
				File dir = new File(rootFolder);
				dir.mkdir();
				String genFolder = rootFolder + "/gen" + g;
				dir = new File(genFolder);
				dir.mkdir();
				// Parameter initialization, random (-1,1)
				double[][] p = new double[M0][N];
				double[] f = new double[M0];
				Random r = new Random();
				r.setSeed(System.currentTimeMillis());
				for (int i = 0; i < M0; i++) {
					for (int j = 0; j < N; j++) {
						p[i][j] = 2 * (r.nextDouble() - 0.5);
					}
				}
				// Writer params to files (used for agent-based simulations
				String allParamFn = rootFolder + "/allparam";
				writeParam(p, allParamFn);
				String paramGen = genFolder + "/param";
				writeParam(p, paramGen);
				writeSingleParam(p, genFolder);
				// Calculated fitness (used in test)
				double meanf = 0;
				for (int i = 0; i < M0; i++) {
					f[i] = calFit(p[i]);
					meanf += f[i];
				}
				meanf /= (double) f.length;
				System.out.println(meanf);
				String fitName = genFolder + "/fitness";
				writeFitness(f, fitName);
				// Pick up best params
				double[][] sortF = sortArray(f);
				f = sortF[0];
				double[] ind = sortF[1];
				// Each winning param generate 4 new params
				double[][] np = new double[M0][N];
				int j = 0;
				for (int i = 0; i < M; i++) {
					for (int rep = 0; rep < M0 / M; rep++) {
						while (true) {
							boolean ifSame = false;
							np[j] = mutSingleParam(p[(int) ind[i]], bound,
									rateMut);
							for (int k = 0; k < p.length; k++) {
								boolean a = compareParam(np[j], p[k]);
								ifSame |= a;
							}
							for (int k = 0; k < j; k++) {
								boolean a = compareParam(np[k], np[j]);
								ifSame |= a;
							}
							if (!ifSame) {
								break;
							}
						}
						j = j + 1;
					}
				}
				// Writer parameters for next generation
				writeParam(np, allParamFn);
				genFolder = rootFolder + "/gen" + (g + 1);
				dir = new File(genFolder);
				dir.mkdir();
				paramGen = genFolder + "/param";
				writeParam(np, paramGen);
				writeSingleParam(np, genFolder);
			} else {
				// Prepare folder and files
				String rootFolder = "ga";
				File dir = new File(rootFolder);
				dir.mkdir();
				String genFolder = rootFolder + "/gen" + g;
				dir = new File(genFolder);
				dir.mkdir();
				// Read in parameters
				double[][] param = new double[M0][N];
				double[][] allParam = new double[M0 * (g + 1)][N];
				// First read in all parameters
				File file = new File(rootFolder + "/allparam");
				if (file.exists()) {
					try {
						Scanner sc = new Scanner(file);
						int i = 0;
						while (sc.hasNext()) {
							String paraline = sc.nextLine();
							Scanner line = new Scanner(paraline)
									.useDelimiter(",");
							int j = 0;
							while (line.hasNext()) {
								allParam[i][j] = line.nextDouble();
								j++;
							}
							i++;
						}
					} catch (FileNotFoundException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				} else {
					System.out.println("No all param files!");
				}
				file = new File(genFolder + "/param");
				if (file.exists()) {
					try {
						Scanner sc = new Scanner(file);
						int i = 0;
						while (sc.hasNext()) {
							String paraline = sc.nextLine();
							Scanner line = new Scanner(paraline)
									.useDelimiter(",");
							int j = 0;
							while (line.hasNext()) {
								param[i][j] = line.nextDouble();
								j++;
							}
							i++;
						}
					} catch (FileNotFoundException e) {
					}
				} else {
					System.out.println("No param file!");
				}
				// Calculated fitness (used in test)
				double[] f = new double[M0];
				double meanf = 0;
				for (int i = 0; i < M0; i++) {
					f[i] = calFit(param[i]);
					meanf += f[i];
				}
				meanf /= (double) f.length;
				System.out.println(meanf);
				String fitName = genFolder + "/fitness";
				writeFitness(f, fitName);
				// Pick up best params
				double[][] sortF = sortArray(f);
				f = sortF[0];
				double[] ind = sortF[1];
				// Each winning param generate 4 new params
				double[][] np = new double[M0][N];
				int j = 0;
				for (int i = 0; i < M; i++) {
					for (int rep = 0; rep < M0 / M; rep++) {
						while (true) {
							boolean ifSame = false;
							np[j] = mutSingleParam(param[(int) ind[i]], bound,
									rateMut);
							for (int k = 0; k < allParam.length; k++) {
								boolean a = compareParam(np[j], allParam[k]);
								ifSame |= a;
							}
							for (int k = 0; k < j; k++) {
								boolean a = compareParam(np[k], np[j]);
								ifSame |= a;
							}
							if (!ifSame) {
								break;
							}
						}
						j = j + 1;
					}
				}
				// Writer parameters for next generation
				writeParam(np, rootFolder + "/allparam");
				genFolder = rootFolder + "/gen" + (g + 1);
				dir = new File(genFolder);
				dir.mkdir();
				String paramGen = genFolder + "/param";
				writeParam(np, paramGen);
				writeSingleParam(np, genFolder);
			}
		}
	}

	public GA(int g) {
		// Number of params
		int N = 9;
		// Number of population
		int M0 = 60;
		// Number of bottleneck population
		int M = 10;
		// Mutation rate
		double rateMut = 0.1;
		String[] pName = new String[N];
		double[][] bound = new double[N][3];
		// 1 = log scale, 0 = normal scale
		int[] logInd = new int[9];
		File file = new File("bound.txt");
		if (file.exists()) {
			try {
				Scanner sc = new Scanner(file);
				int i = 0;
				while (sc.hasNext()) {
					String paraline = sc.nextLine();
					Scanner line = new Scanner(paraline).useDelimiter("=");
					pName[i] = line.next();
					String dataline = line.next();
					Scanner sc1 = new Scanner(dataline).useDelimiter(";");
					String data = sc1.next();
					logInd[i] = sc1.nextInt();
					Scanner sc2 = new Scanner(data).useDelimiter(",");
					int j = 0;
					// Upper bound, lower bound, step size
					while (sc2.hasNext()) {
						bound[i][j] = sc2.nextDouble();
						j++;
					}
					i++;
				}

			} catch (FileNotFoundException e) {
			}
		} else {
			System.out.println("No parameter range file found!");
		}
		if (g == 0) {
			// Prepare folder and files
			String rootFolder = "ga";
			File dir = new File(rootFolder);
			dir.mkdir();
			String genFolder = rootFolder + "/gen" + g;
			dir = new File(genFolder);
			dir.mkdir();
			// Parameter initialization, random (lowbound,upbound)
			double[][] p = new double[M0][N];
			Random r = new Random();
			r.setSeed(System.currentTimeMillis());
			for (int i = 0; i < M0; i++) {
				for (int j = 0; j < N; j++) {
					p[i][j] = r.nextDouble() * (bound[j][0] - bound[j][1])
							+ bound[j][1];
				}
			}
			// Writer params to files (used for agent-based simulations)
			String allParamFn = rootFolder + "/allparam";
			writeParam(p, allParamFn);
			String paramGen = genFolder + "/param";
			writeParam(p, paramGen);
			writeSingleParam(p, logInd, pName, genFolder);
		} else {
			// Prepare folder and files
			String rootFolder = "ga";
			File dir = new File(rootFolder);
			dir.mkdir();
			String genFolder = rootFolder + "/gen" + g;
			dir = new File(genFolder);
			dir.mkdir();
			// Read in parameters
			double[][] param = new double[M0][N];
			double[][] allParam = new double[M0 * (g + 1)][N];
			// First read in all parameters
			file = new File(rootFolder + "/allparam");
			if (file.exists()) {
				try {
					Scanner sc = new Scanner(file);
					int i = 0;
					while (sc.hasNext()) {
						String paraline = sc.nextLine();
						Scanner line = new Scanner(paraline).useDelimiter(",");
						int j = 0;
						while (line.hasNext()) {
							allParam[i][j] = line.nextDouble();
							j++;
						}
						i++;
					}
				} catch (FileNotFoundException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			} else {
				System.out.println("No all param files!");
			}
			file = new File(rootFolder + "/gen" + (g-1) + "/param");
			if (file.exists()) {
				try {
					Scanner sc = new Scanner(file);
					int i = 0;
					while (sc.hasNext()) {
						String paraline = sc.nextLine();
						Scanner line = new Scanner(paraline).useDelimiter(",");
						int j = 0;
						while (line.hasNext()) {
							param[i][j] = line.nextDouble();
							j++;
						}
						i++;
					}
				} catch (FileNotFoundException e) {
				}
			} else {
				System.out.println("No param file!");
			}
			// Read in fitness file in previous generation folder
			double[] f = new double[M0];
			String pastGenFolder = rootFolder + "/gen" + (g - 1);
			File fitfn = new File(pastGenFolder + "/fitness");
			Scanner sc;
			try {
				sc = new Scanner(fitfn);
				int i = 0;
				while (sc.hasNext()) {
					String paraline = sc.nextLine();
					Scanner line = new Scanner(paraline);
					f[i] = line.nextDouble();
					i++;
				}
			} catch (FileNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			// Pick up best params
			double[][] sortF = sortArray(f);
			f = sortF[0];
			double[] ind = sortF[1];
			// Each winning param generate M0/M new params
			double[][] np = new double[M0][N];
			int j = 0;
			for (int i = 0; i < M; i++) {
				for (int rep = 0; rep < M0 / M; rep++) {
					while (true) {
						boolean ifSame = false;
						np[j] = mutSingleParam(param[(int) ind[i]], bound,
								rateMut);
						for (int k = 0; k < allParam.length; k++) {
							boolean a = compareParam(np[j], allParam[k]);
							ifSame |= a;
						}
						for (int k = 0; k < j; k++) {
							boolean a = compareParam(np[k], np[j]);
							ifSame |= a;
						}
						if (!ifSame) {
							break;
						}
					}
					j = j + 1;
				}
			}
			// Writer parameters for this generation (used for agent-based model
			String allParamFn = rootFolder + "/allparam";
			writeParam(np, allParamFn);
			String paramGen = genFolder + "/param";
			writeParam(np, paramGen);
			writeSingleParam(np, logInd, pName, genFolder);
		}

	}

	public boolean compareParam(double[] a, double[] b) {
		if (a.length != b.length) {
			System.err
					.println("Two input parameter set length should be equal!");
			return false;
		} else {
			double sum = 0;
			for (int i = 0; i < a.length; i++) {
				sum += (a[i] - b[i]) * (a[i] - b[i]);
			}
			sum = Math.sqrt(sum);
			if (sum < 0.00000001) {
				return true;
			} else {
				return false;
			}
		}
	}

	public void printMatrix(double[][] a) {
		for (int i = 0; i < a.length; i++) {
			for (int j = 0; j < a[0].length; j++) {
				System.out.print(a[i][j] + " ");
			}
			System.out.println();
		}
	}

	public static void main(String[] args) {
		int g = Integer.parseInt(args[0]);
		new GA(g);
	}
}
